package controller

import (
	"context"
	"fmt"
	appsv1 "k8s.io/api/apps/v1"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"strings"
	"time"
	"v8s-controller/generator"

	"v8s-watcher/internal/db"
	"v8s-watcher/internal/k8s"
	"v8s/logger"

	corev1 "k8s.io/api/core/v1"
)

type (
	StatusController struct {
		*generic

		projects  *db.ProjectsRepository
		k8sClient *k8s.Repository
	}
)

const (
	statusControllerName = "status-controller"
)

func CreateStatusController(projects *db.ProjectsRepository, k8sClient *k8s.Repository) *StatusController {
	return &StatusController{
		generic: &generic{
			log: logger.Logger().Named(statusControllerName).Sugar(),
		},

		projects:  projects,
		k8sClient: k8sClient,
	}
}

func (s *StatusController) Name() string {
	return statusControllerName
}

func (s *StatusController) Execute(ctx context.Context) error {
	startedAt := time.Now()

	return s.withChannel(s.projects.ListProjectsSince(ctx, startedAt, 20), func(project *db.Project) (bool, error) {
		handled := false

		for _, task := range project.Tasks {
			current := task

			if current.Status != db.RunningTaskStatus {

				current.Clusters = map[db.ClusterName]db.ClusterDetails{}

				continue
			}

			namespace, err := current.ID.ToNamespace()
			if err != nil {
				s.log.Errorf("failed to convert task-id '%s' to namespace: %s", current.ID, err)
				current.Clusters = map[db.ClusterName]db.ClusterDetails{"v8s-cluster-0": db.ClusterDetails{Status: db.ClusterStatusError}}

				continue
			}

			pods, err := s.k8sClient.GetPods(ctx, namespace)
			if err != nil {
				s.log.Errorf("failed to obtain pods in namespace '%s': %s", namespace, err)
				current.Clusters = map[db.ClusterName]db.ClusterDetails{"v8s-cluster-0": db.ClusterDetails{Status: db.ClusterStatusError}}

				continue
			}

			services, err := s.k8sClient.GetServices(ctx, namespace)
			if err != nil {
				s.log.Errorf("failed to obtain services in namespace '%s': %s", namespace, err)
				current.Clusters = map[db.ClusterName]db.ClusterDetails{"v8s-cluster-0": db.ClusterDetails{Status: db.ClusterStatusError}}
				continue
			}

			pvcs, err := s.k8sClient.GetVolumeClaims(ctx, namespace)
			if err != nil {
				s.log.Errorf("failed to obtain volume claims in namespace '%s': %s", namespace, err)
				current.Clusters = map[db.ClusterName]db.ClusterDetails{"v8s-cluster-0": db.ClusterDetails{Status: db.ClusterStatusError}}

				continue
			}
			deployments, err := s.k8sClient.GetDeployments(ctx, namespace)
			if err != nil {
				s.log.Errorf("failed to obtain deployments in namespace '%s': %s", namespace, err)
				current.Clusters = map[db.ClusterName]db.ClusterDetails{"v8s-cluster-0": db.ClusterDetails{Status: db.ClusterStatusError}}

				continue
			}
			clusterDetails := s.calculateClusterDetails(current, pods, services, pvcs, deployments)

			current.Clusters = map[db.ClusterName]db.ClusterDetails{"v8s-cluster-0": clusterDetails}

			handled = true

			s.log.Debugf("task '%s' have clusters '%+v', services '%+v'", current.ID, current.Clusters, current.Compose)
		}

		return handled, s.projects.SaveProject(ctx, project)
	})
}
func (s *StatusController) calculateClusterDetails(
	task *db.Task,
	pods []corev1.Pod,
	services []corev1.Service,
	pvcs []corev1.PersistentVolumeClaim,
	deployments []appsv1.Deployment,
) db.ClusterDetails {
	result := db.ClusterDetails{}

	resources := make([]db.Resource, 0, len(pods)+len(services)+len(pvcs)+len(deployments))

	for _, pod := range pods {
		resources = append(resources, db.Resource{
			Kind:      "Pod",
			Name:      pod.ObjectMeta.Name,
			BelongsTo: pod.Annotations[generator.ResourceBelongsToAnnotation],
			Status:    db.ResourceStatus(pod.Status.Phase)})
	}

	for _, pvc := range pvcs {
		resources = append(resources, db.Resource{
			Kind:      "PersistentVolumeClaim",
			Name:      pvc.ObjectMeta.Name,
			BelongsTo: pvc.Annotations[generator.ResourceBelongsToAnnotation],
			Status:    db.ResourceStatus(pvc.Status.Phase)})
	}

	for _, service := range services {
		if service.Spec.Type != corev1.ServiceTypeLoadBalancer {
			continue
		}

		resource := db.Resource{
			Kind:      "Service",
			Name:      service.Name,
			BelongsTo: service.Annotations[generator.ResourceBelongsToAnnotation],
		}

		ingress := service.Status.LoadBalancer.Ingress

		if len(ingress) == 0 || service.Spec.LoadBalancerIP == "" {
			resource.Status = db.ResourceStatusPending
		} else {
			resource.Status = db.ResourceStatusRunning
		}

		resources = append(resources, resource)

		if resource.Status != db.ResourceStatusRunning {
			continue
		}

		for composeService := range task.Compose.Services {
			if !isBelongsTo(service, composeService) {
				s.log.Infof("load balancer '%s' does not belongs to service '%s'", service.Name, composeService)
				continue
			}

			if result.Services == nil {
				result.Services = make(map[db.ServiceName]db.ServiceInfo)
			}

			ports := strings.Split(service.Annotations[fmt.Sprintf(generator.LoadBalancerServicePortsTemplate, composeService)], generator.LoadBalancerServicePortsValueDelimiter)
			endpoints := make([]string, 0, len(ports))

			for _, port := range ports {
				endpoints = append(endpoints, fmt.Sprintf("%s:%s", service.Spec.LoadBalancerIP, port))
			}

			result.Services[db.ServiceName(composeService)] = db.ServiceInfo{Endpoints: endpoints}
		}
	}

	for _, deployment := range deployments {
		resource := db.Resource{
			Kind:      "Deployment",
			Name:      deployment.ObjectMeta.Name,
			BelongsTo: deployment.Annotations[generator.ResourceBelongsToAnnotation],
			Status:    db.ResourceStatusRunning,
		}
		for _, condition := range deployment.Status.Conditions {
			if condition.Type == appsv1.DeploymentReplicaFailure && v1.ConditionStatus(condition.Status) == v1.ConditionTrue {
				resource.Status = db.ResourceStatusError
				break
			}
		}

		resources = append(resources, resource)
	}

	status := db.ClusterStatusRunning
	for _, r := range resources {
		if r.Status == db.ResourceStatusError || r.Status == db.ResourceStatusLost ||
			r.Status == db.ResourceStatusUnknown || r.Status == db.ResourceStatusSucceeded {
			status = db.ClusterStatusError
			break
		} else if r.Status == db.ResourceStatusPending {
			status = db.ClusterStatusStarting
		}
	}

	result.Status = status
	result.Resources = resources

	return result
}

func isBelongsTo(service corev1.Service, composeService string) bool {
	parts := strings.Split(service.Annotations[generator.ResourceBelongsToAnnotation], generator.ResourceBelongsToAnnotationValueDelimiter)

	result := false

	for _, part := range parts {
		if part == composeService {
			result = true
			break
		}
	}

	return result
}
